Welcome to the Learning Edition of the SuiteQL Query Tool. This guide will help you navigate the codebase and understand the key SuiteScript concepts demonstrated throughout the application.
Want to see something working right away? Here's the fastest path to understanding how this tool works.
Open the SuiteQL Query Tool and paste this simple query into the editor:
SELECT
ID,
CompanyName,
Email
FROM
Customer
WHERE
ROWNUM <= 10
Click Run (or press Ctrl+Enter). You should see 10 customer records appear in the results.
Here's what just occurred behind the scenes:
handlePostRequest() function received itexecuteQuery() which used query.runSuiteQL() to execute your SQLOpen suiteql-query-tool.v2026.01.suitelet-learning.js and search for:
SECTION 4: QUERY EXECUTION - Where the query runs on the serverquery.runSuiteQL - The actual NetSuite API callfunction runQueryWithText - The client-side JavaScript that sends the requestNow experiment! Try these modifications:
Customer to Vendor or EmployeePhone, DateCreatedROWNUM <= 10 to ROWNUM <= 25AND IsInactive = 'F'The Learning Edition is a fully-annotated version of the SuiteQL Query Tool, designed to help developers learn SuiteScript development through a real-world, production-quality application.
SUITESCRIPT LEARNING NOTES in the code. These contain detailed explanations of NetSuite-specific concepts.
Before diving into the code, here's a quick overview of SuiteScript fundamentals.
SuiteScript is NetSuite's server-side JavaScript API. It allows you to customize and extend NetSuite's functionality through scripts that run on NetSuite's servers. SuiteScript 2.x (the current version) uses the AMD module pattern and supports modern JavaScript features.
NetSuite has several script types, each designed for specific use cases:
| Script Type | Purpose | Entry Point(s) |
|---|---|---|
| Suitelet | Custom pages and API endpoints | onRequest |
| User Event | Triggers on record events | beforeLoad, beforeSubmit, afterSubmit |
| Client Script | Browser-side form interactions | pageInit, saveRecord, fieldChanged, etc. |
| Scheduled | Background processing on a schedule | execute |
| Map/Reduce | Large-scale data processing | getInputData, map, reduce, summarize |
| RESTlet | REST API endpoints | get, post, put, delete |
The SuiteQL Query Tool is a Suitelet, which makes it perfect for building custom web applications inside NetSuite.
SuiteScript functionality is organized into modules prefixed with "N/". You import these modules using the AMD define() function:
define([
'N/query', // SuiteQL queries
'N/file', // File Cabinet operations
'N/runtime' // Script/user/session info
], function(query, file, runtime) {
// Your code here
return {
onRequest: myFunction
};
});
Every SuiteScript operation consumes "governance units." Each script type has a limit:
| Script Type | Governance Limit |
|---|---|
| Suitelet | 1,000 units |
| User Event | 1,000 units |
| Client Script | 1,000 units |
| Scheduled Script | 10,000 units |
| Map/Reduce | 10,000 units per phase |
| RESTlet | 5,000 units |
query.runSuiteQL() = 10 unitsfile.load() = 10 unitsfile.save() = 10 unitshttps.post() = 10 unitsrecord.load() = 5-10 unitslog.debug() = 0 units
The SuiteQL Query Tool follows a client-server architecture where a single Suitelet serves both the HTML user interface and a JSON API.
┌─────────────────────────────────────────────────────────────────────┐
│ USER'S BROWSER │
└─────────────────────────────────────────────────────────────────────┘
│ ▲
│ 1. GET request │ 2. HTML page with
│ (initial page load) │ embedded JavaScript
▼ │
┌─────────────────────────────────────────────────────────────────────┐
│ SUITELET │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ onRequest(context) │ │
│ │ ├── GET → handleGetRequest() → serverWidget form │ │
│ │ └── POST → handlePostRequest() → JSON response │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
▲ │
│ 3. AJAX POST │ 4. JSON data
│ { function: 'queryExecute', ... } │ { records: [...] }
│ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ BROWSER JAVASCRIPT (SQT) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ runQuery() │ │ loadSqlFile() │ │ generateAI() │ │
│ │ → fetch() POST │ │ → fetch() POST │ │ → fetch() POST │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
The Suitelet handles two types of requests:
Instead of using NetSuite's built-in form widgets, we inject a complete custom HTML application using the INLINEHTML field type. This gives us full control over the user interface.
The embedded JavaScript runs in the browser (not as SuiteScript). It uses standard web APIs like fetch() to communicate with the server. This is regular JavaScript with access to the DOM, localStorage, and third-party libraries.
POST requests include a function property that tells the server which operation to perform. The server uses a dispatch table to route requests:
const handlers = {
'queryExecute': () => executeQuery(context, payload),
'sqlFileLoad': () => loadSqlFile(context, payload),
'sqlFileSave': () => saveSqlFile(context, payload),
// ... more handlers
};
const handler = handlers[payload.function];
if (handler) handler();
The Learning Edition script is organized into numbered sections. Here's what each section contains:
Object.freeze() for immutable configdefine() call, module imports, and entry point returnhandleGetRequest() and handlePostRequest() - the routing layerN/query, pagination, virtual viewsN/file - load, save, list filesN/https to AI servicesN/render and session storage with N/runtimeThese are the most instructive parts of the codebase for learning SuiteScript:
Location: Section 4, executeQuery() function
Learn how to execute SuiteQL queries and handle the results:
const records = modules.query.runSuiteQL({
query: 'SELECT ID, CompanyName FROM Customer WHERE IsInactive = ?',
params: ['F'] // Parameterized query - prevents SQL injection!
}).asMappedResults(); // Returns array of objects
Location: Section 5, loadSqlFile() and saveSqlFile()
Learn how to read and write files in NetSuite's File Cabinet:
// Loading a file
const fileObj = modules.file.load({ id: fileId });
const contents = fileObj.getContents();
// Creating a file
const newFile = modules.file.create({
name: 'query.sql',
contents: sqlText,
folder: folderId,
fileType: modules.file.Type.PLAINTEXT
});
const savedId = newFile.save();
Location: Section 6.5, callAnthropicAPI() and callOpenAIAPI()
Learn how to make HTTPS requests to external services:
const response = modules.https.post({
url: 'https://api.example.com/endpoint',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
body: JSON.stringify(requestData)
});
if (response.code === 200) {
const data = JSON.parse(response.body);
}
Location: Section 7, submitDocument() and generateDocument()
Learn how to persist data across requests within a user's session:
// Storing data
const session = modules.runtime.getCurrentSession();
session.set({
name: 'myData',
value: JSON.stringify(dataObject)
});
// Retrieving data (in a later request)
const stored = session.get({ name: 'myData' });
const data = JSON.parse(stored);
Location: Section 7, generateDocument()
Learn how to generate PDFs from templates with dynamic data:
const renderer = modules.render.create();
renderer.addCustomDataSource({
alias: 'data',
format: modules.render.DataSource.OBJECT,
data: { records: queryResults }
});
renderer.templateContent = xmlTemplate;
const pdfFile = renderer.renderAsPdf();
Location: Section 4, executePaginatedQuery()
Learn how to retrieve more than SuiteQL's 5,000 row limit:
// Wrap query with ROWNUM for pagination
const paginatedSql = `
SELECT * FROM (
SELECT ROWNUM AS ROWNUMBER, * FROM (${originalQuery})
) WHERE ROWNUMBER BETWEEN ${start} AND ${end}
`;
Location: Section 9, generateMainHtml() and generateClientScript()
Learn the pattern for building single-page applications inside Suitelets using INLINEHTML, including client-server communication with fetch().
Here's a recommended order for exploring the codebase:
NetSuite has its own vocabulary. Here are key terms you'll encounter:
| Term | Definition |
|---|---|
| Internal ID | A unique numeric identifier assigned to every record in NetSuite. Used in scripts to reference specific records (e.g., Customer ID 12345). |
| Script ID | A human-readable identifier you assign when creating scripts, custom fields, or custom records. Always prefixed (e.g., customscript_my_suitelet, custbody_my_field). |
| Deployment | An instance of a script configured to run. One script can have multiple deployments with different settings, permissions, or triggers. |
| File Cabinet | NetSuite's file storage system. Organized into folders, stores scripts, images, documents, and other files. |
| Governance | NetSuite's resource management system. Each script operation costs "units" and scripts have limits to prevent runaway processes. |
| Record Type | A category of data in NetSuite (e.g., Customer, Sales Order, Invoice). Each has specific fields and behaviors. |
| Subsidiary | A company entity within NetSuite. Used for multi-company or multi-country setups. Data is often filtered by subsidiary. |
| Role | A set of permissions assigned to users. Determines what records and scripts a user can access. |
| Saved Search | NetSuite's original query system (pre-SuiteQL). Still widely used but being superseded by SuiteQL for complex queries. |
| Workbook | NetSuite's visual query builder (Analytics). Can be converted to SuiteQL using query.load().toSuiteQL(). |
| SuiteApp / Bundle | A packaged set of customizations (scripts, records, fields) that can be installed as a unit. |
| Sandbox | A copy of your production NetSuite account for testing. Changes here don't affect production. |
| SDF (SuiteCloud Development Framework) | NetSuite's modern development tooling for source control, IDE integration, and deployment automation. |
| BUILTIN.DF() | "Display Field" - A SuiteQL function that returns the display name instead of the internal ID for reference fields. |
| Custom Record | A user-defined record type for storing data that doesn't fit standard records. Tables are named customrecord_xxx. |
Here's how to deploy the SuiteQL Query Tool (or any Suitelet) to NetSuite:
suiteql-query-tool.v2026.01.suitelet.jsSuiteScripts/suiteql-query-tool.v2026.01.suitelet.js)_suiteql_query_tool (NetSuite adds customscript prefix)_suiteql_query_tool (NetSuite adds customdeploy prefix)After saving the deployment, you can access the Suitelet via:
https://[account-id].app.netsuite.com/app/site/hosting/scriptlet.nl?script=123&deploy=1script and deploy parameters are the internal IDs of the script and deployment records.
To add a menu item for easy access:
Here are errors you'll commonly encounter and how to resolve them:
Cause: Your script used more governance units than its limit allows.
Solutions:
runtime.getCurrentScript().getRemainingUsage() to monitor usageCause: The column doesn't exist on the table, or you don't have permission to access it.
Solutions:
BUILTIN.DF() for reference fields if you want the display valueCause: The table doesn't exist or you don't have permission to access it.
Solutions:
customrecord_xxx)Cause: A generic error, often from malformed queries or server issues.
Solutions:
Cause: The query took too long to execute (usually over 3-5 minutes).
Solutions:
SELECT * - only select columns you needCause: The AI API key is incorrect, expired, or has insufficient permissions.
Solutions:
Cause: Trying to load a file that doesn't exist or was deleted.
Solutions:
A handy reference for common SuiteQL patterns and syntax.
| Table | Description | Key Columns |
|---|---|---|
Transaction |
All transactions (orders, invoices, etc.) | id, tranid, type, entity, trandate, status, total |
TransactionLine |
Line items on transactions | id, transaction, item, quantity, amount, rate |
Customer |
Customer records | id, entityid, companyname, email, salesrep |
Vendor |
Vendor records | id, entityid, companyname, email |
Employee |
Employee records | id, entityid, firstname, lastname, email, department |
Item |
All item types | id, itemid, displayname, baseprice, itemtype |
Account |
Chart of accounts | id, acctnumber, acctname, accttype |
Department |
Department records | id, name, parent, isinactive |
Location |
Location records | id, name, parent, isinactive |
Subsidiary |
Subsidiary records | id, name, parent, isinactive |
File |
File Cabinet files | id, name, folder, filetype, url |
Use these in WHERE Transaction.type = 'xxx':
| Code | Transaction Type |
|---|---|
SalesOrd | Sales Order |
Invoice | Invoice |
CustInvc | Customer Invoice |
CashSale | Cash Sale |
CustPymt | Customer Payment |
CustCred | Credit Memo |
PurchOrd | Purchase Order |
VendBill | Vendor Bill |
VendPymt | Vendor Payment |
VendCred | Vendor Credit |
Journal | Journal Entry |
Check | Check |
Deposit | Deposit |
Transfer | Transfer |
ItemRcpt | Item Receipt |
ItemShip | Item Fulfillment |
RtnAuth | Return Authorization |
Estimate | Quote/Estimate |
Opportunty | Opportunity |
-- Get display value instead of internal ID
BUILTIN.DF( Transaction.status ) AS StatusName
-- Handle NULL values
NVL( Customer.phone, 'No Phone' ) AS Phone
-- Date formatting
TO_CHAR( Transaction.trandate, 'YYYY-MM-DD' ) AS FormattedDate
-- Date parsing
TO_DATE( '2024-01-15', 'YYYY-MM-DD' )
-- Current date
SYSDATE
-- Date arithmetic
ADD_MONTHS( SYSDATE, -3 ) -- 3 months ago
TRUNC( SYSDATE ) -- Today at midnight
-- String functions
UPPER( Customer.companyname )
LOWER( Customer.email )
SUBSTR( Item.itemid, 1, 3 )
CONCAT( firstname, CONCAT( ' ', lastname ) )
-- Conditional logic
CASE
WHEN amount > 1000 THEN 'Large'
WHEN amount > 100 THEN 'Medium'
ELSE 'Small'
END AS SizeCategory
-- Aggregates
COUNT( * ), SUM( amount ), AVG( rate ), MIN( trandate ), MAX( trandate )
SELECT
Transaction.tranid,
Transaction.trandate,
BUILTIN.DF( Transaction.entity ) AS CustomerName,
Transaction.total
FROM
Transaction
INNER JOIN Customer ON Customer.id = Transaction.entity
WHERE
Transaction.type = 'SalesOrd'
AND Transaction.trandate >= TO_DATE( '2024-01-01', 'YYYY-MM-DD' )
ORDER BY
Transaction.trandate DESC
SELECT
Transaction.tranid,
TransactionLine.linesequencenumber,
BUILTIN.DF( TransactionLine.item ) AS ItemName,
TransactionLine.quantity,
TransactionLine.rate,
TransactionLine.amount
FROM
Transaction
INNER JOIN TransactionLine ON TransactionLine.transaction = Transaction.id
WHERE
Transaction.id = 12345
SELECT
BUILTIN.DF( Transaction.status ) AS Status,
COUNT( * ) AS OrderCount,
SUM( Transaction.total ) AS TotalAmount
FROM
Transaction
WHERE
Transaction.type = 'SalesOrd'
GROUP BY
BUILTIN.DF( Transaction.status )
ORDER BY
OrderCount DESC
SELECT * FROM (
SELECT
Customer.id,
Customer.companyname
FROM
Customer
ORDER BY
Customer.datecreated DESC
)
WHERE ROWNUM <= 10
Security is critical when building NetSuite applications. Follow these practices:
Never concatenate user input directly into SQL strings. Use the params array:
// GOOD - Parameterized (safe)
const results = query.runSuiteQL({
query: 'SELECT * FROM Customer WHERE id = ?',
params: [customerId]
}).asMappedResults();
// BAD - String concatenation (SQL injection risk!)
const results = query.runSuiteQL({
query: 'SELECT * FROM Customer WHERE id = ' + customerId
}).asMappedResults();
customerId contains 1 OR 1=1, the unsafe version returns ALL customers. Parameterized queries treat the value as data, not SQL code.
API keys should be:
When creating files with file.create():
const fileObj = modules.file.create({
name: 'report.pdf',
contents: pdfContents,
folder: folderId,
fileType: modules.file.Type.PDF,
isOnline: false // IMPORTANT: Keep files private!
});
Setting isOnline: true makes the file publicly accessible without authentication. Only use this for intentionally public files.
Always validate data from external sources:
// Validate expected data types
if (typeof payload.customerId !== 'number') {
throw new Error('Invalid customer ID');
}
// Validate against allowed values
const allowedTypes = ['SalesOrd', 'Invoice', 'PurchOrd'];
if (!allowedTypes.includes(payload.transactionType)) {
throw new Error('Invalid transaction type');
}
Use log.audit() for security-relevant events:
modules.log.audit({
title: 'Sensitive Operation',
details: `User ${runtime.getCurrentUser().id} exported ${records.length} customer records`
});
Effective debugging is essential for SuiteScript development. Here are key techniques:
The N/log module writes to NetSuite's Execution Log:
// Different log levels
log.debug({ title: 'Debug Info', details: myVariable });
log.audit({ title: 'Important Event', details: 'User clicked submit' });
log.error({ title: 'Error Occurred', details: e.message });
// Log objects (automatically stringified)
log.debug({ title: 'Query Results', details: records });
// Log governance usage
log.debug({
title: 'Governance Check',
details: 'Remaining: ' + runtime.getCurrentScript().getRemainingUsage()
});
To view logs: Go to the Script Deployment record → click View Execution Log
For JavaScript running in the browser (inside INLINEHTML):
console.log() for debuggingWhen debugging complex queries or operations:
log.debug({ title: 'Before Query', details: { query: sql, params: params } });
const results = query.runSuiteQL({ query: sql, params: params }).asMappedResults();
log.debug({ title: 'After Query', details: { count: results.length } });
try {
// Your code here
} catch (e) {
log.error({
title: 'Error in myFunction',
details: {
message: e.message,
name: e.name,
stack: e.stack
}
});
throw e; // Re-throw if needed
}
const DEBUG = true; // Set to false in production
if (DEBUG) {
log.debug({ title: 'Detailed Debug', details: bigObject });
}
Always test scripts in a Sandbox account before deploying to production:
NetSuite has a built-in debugger for server-side SuiteScript:
A: SuiteQL is based on Oracle SQL, which doesn't have a LIMIT keyword. Use ROWNUM instead:
-- Get first 10 rows
SELECT * FROM Customer WHERE ROWNUM <= 10
-- For pagination (rows 11-20), wrap in subquery
SELECT * FROM (
SELECT ROWNUM AS rn, c.* FROM Customer c
) WHERE rn BETWEEN 11 AND 20
A: Several options:
SELECT * FROM OA_TABLES WHERE ROWNUM <= 100A: Common causes and fixes:
trandate BETWEEN x AND y for transactionsWHERE UPPER(name) = 'X' can't use indexesA: Reference fields (foreign keys) store internal IDs. Use BUILTIN.DF() to get the display value:
-- Returns ID like "42"
SELECT status FROM Transaction
-- Returns name like "Pending Fulfillment"
SELECT BUILTIN.DF(status) AS StatusName FROM Transaction
A: No. SuiteQL is read-only. To modify data, use the N/record module:
const rec = record.load({ type: 'customer', id: 123 });
rec.setValue({ fieldId: 'phone', value: '555-1234' });
rec.save();
A: SuiteScript 2.x (current) uses modern JavaScript with AMD modules. Key differences:
| Feature | SuiteScript 1.0 | SuiteScript 2.x |
|---|---|---|
| Module System | Global functions | AMD (define/require) |
| JavaScript | ES3/ES5 | ES6+ (with 2.1) |
| API Style | nlapiLoadRecord() |
record.load() |
| Status | Legacy (avoid) | Current (use this) |
A: Check remaining units with:
const remaining = runtime.getCurrentScript().getRemainingUsage();
log.debug('Governance remaining', remaining);
// Bail out if running low
if (remaining < 100) {
throw new Error('Low governance - stopping early');
}
A: Not directly. SuiteScript runs in NetSuite's environment, not Node.js. However:
A: Create a Scheduled Script:
@NScriptType ScheduledScript in your fileexecute functionA: Query history is stored in your browser's localStorage. It persists across sessions but is specific to your browser and device. Clearing browser data will erase history.
A: Yes! Use the Share button to generate a URL that includes your query. Anyone with access to the tool can open that URL and see your query.
A: The AI features use external services (Anthropic Claude or OpenAI). You provide your own API key, which keeps costs transparent and data under your control. Keys are stored in your browser session, not on NetSuite servers.
A condensed reference you can print and keep nearby. (Tip: Use your browser's Print function and select "Print selection" if available.)
Transaction | Orders, invoices, etc. |
TransactionLine | Line items |
Customer | Customers |
Vendor | Vendors |
Employee | Employees |
Item | All items |
Account | GL accounts |
SalesOrd | Sales Order |
Invoice | Invoice |
PurchOrd | Purchase Order |
VendBill | Vendor Bill |
Journal | Journal Entry |
CustPymt | Customer Payment |
N/query | SuiteQL queries |
N/record | CRUD operations |
N/file | File Cabinet |
N/https | External APIs |
N/runtime | Script/user info |
N/log | Logging |
BUILTIN.DF(field) -- Display value
NVL(field, default) -- Handle NULL
TO_DATE('2024-01-01', 'YYYY-MM-DD')
TO_CHAR(date, 'YYYY-MM-DD')
SYSDATE -- Current date
ROWNUM -- Row limiter
SELECT
t.tranid,
BUILTIN.DF(t.entity) AS Name
FROM Transaction t
WHERE t.type = 'SalesOrd'
AND t.trandate >= TO_DATE(...)
ORDER BY t.trandate DESC
query.runSuiteQL() | 10 units |
record.load() | 5-10 units |
file.load() | 10 units |
https.post() | 10 units |
log.*() | 0 units |
| Suitelet | Custom pages/APIs |
| User Event | Record triggers |
| Scheduled | Background jobs |
| Map/Reduce | Large data processing |
| RESTlet | REST endpoints |
params: [] for parameterized queries (SQL injection prevention)'T' and 'F', not true/false